/*
 * DefaultInputHandler.java - Default implementation of an input handler
 * Copyright (C) 1999 Slava Pestov
 *
 * You may use and modify this package for any purpose. Redistribution is
 * permitted, in both source and binary form, provided that this notice
 * remains intact in all source distributions of this package.
 */

package org.syntax.jedit;

import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.Hashtable;
import java.util.StringTokenizer;

import javax.swing.KeyStroke;

/**
 * The default input handler. It maps sequences of keystrokes into actions and
 * inserts key typed events into the text area.
 * 
 * @author Slava Pestov
 * @version $Id: DefaultInputHandler.java,v 1.18 1999/12/13 03:40:30 sp Exp $
 */
public class DefaultInputHandler extends InputHandler
{
    /**
     * Creates a new input handler with no key bindings defined.
     */
    public DefaultInputHandler()
    {
        bindings = currentBindings = new Hashtable();
    }

    /**
     * Sets up the default key bindings.
     */
    public void addDefaultKeyBindings()
    {
        addKeyBinding("BACK_SPACE", BACKSPACE);
        addKeyBinding("C+BACK_SPACE", BACKSPACE_WORD);
        addKeyBinding("DELETE", DELETE);
        addKeyBinding("C+DELETE", DELETE_WORD);

        addKeyBinding("ENTER", INSERT_BREAK);
        addKeyBinding("TAB", INSERT_TAB);

        addKeyBinding("INSERT", OVERWRITE);
        addKeyBinding("C+\\", TOGGLE_RECT);

        addKeyBinding("HOME", HOME);
        addKeyBinding("END", END);
        addKeyBinding("C+A", SELECT_ALL);
        addKeyBinding("S+HOME", SELECT_HOME);
        addKeyBinding("S+END", SELECT_END);
        addKeyBinding("C+HOME", DOCUMENT_HOME);
        addKeyBinding("C+END", DOCUMENT_END);
        addKeyBinding("CS+HOME", SELECT_DOC_HOME);
        addKeyBinding("CS+END", SELECT_DOC_END);

        addKeyBinding("PAGE_UP", PREV_PAGE);
        addKeyBinding("PAGE_DOWN", NEXT_PAGE);
        addKeyBinding("S+PAGE_UP", SELECT_PREV_PAGE);
        addKeyBinding("S+PAGE_DOWN", SELECT_NEXT_PAGE);

        addKeyBinding("LEFT", PREV_CHAR);
        addKeyBinding("S+LEFT", SELECT_PREV_CHAR);
        addKeyBinding("C+LEFT", PREV_WORD);
        addKeyBinding("CS+LEFT", SELECT_PREV_WORD);
        addKeyBinding("RIGHT", NEXT_CHAR);
        addKeyBinding("S+RIGHT", SELECT_NEXT_CHAR);
        addKeyBinding("C+RIGHT", NEXT_WORD);
        addKeyBinding("CS+RIGHT", SELECT_NEXT_WORD);
        addKeyBinding("UP", PREV_LINE);
        addKeyBinding("S+UP", SELECT_PREV_LINE);
        addKeyBinding("DOWN", NEXT_LINE);
        addKeyBinding("S+DOWN", SELECT_NEXT_LINE);

        addKeyBinding("C+ENTER", REPEAT);
        addKeyBinding("S+ENTER", INSERT_CR_BREAK);

        // Clipboard
        addKeyBinding("C+C", CLIP_COPY);
        addKeyBinding("C+V", CLIP_PASTE);
        addKeyBinding("C+X", CLIP_CUT);

        // undo & redo
        addKeyBinding("C+Z", UNDO);
        addKeyBinding("C+Y", REDO);

        addKeyBinding("C+S", SAVE);
        
        addKeyBinding("C+F", FIND);
        
        /*
         * MIRTH-1225 and MIRTH-2019: Sometimes CTRL might not be the same as
         * the OS defined menu shortcut key mask. For example, this is COMMAND
         * on OSX. In this case, we want both CTRL and COMMAND to work.
         */
        if (InputEvent.CTRL_MASK != Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()) {
            addKeyBinding("X+BACK_SPACE", BACKSPACE_WORD);
            addKeyBinding("X+DELETE", DELETE_WORD);
            addKeyBinding("X+\\", TOGGLE_RECT);
            addKeyBinding("X+A", SELECT_ALL);
            addKeyBinding("X+HOME", DOCUMENT_HOME);
            addKeyBinding("X+END", DOCUMENT_END);
            addKeyBinding("XS+HOME", SELECT_DOC_HOME);
            addKeyBinding("XS+END", SELECT_DOC_END);
            addKeyBinding("X+LEFT", PREV_WORD);
            addKeyBinding("XS+LEFT", SELECT_PREV_WORD);
            addKeyBinding("X+RIGHT", NEXT_WORD);
            addKeyBinding("XS+RIGHT", SELECT_NEXT_WORD);
            addKeyBinding("X+ENTER", REPEAT);
            addKeyBinding("X+C", CLIP_COPY);
            addKeyBinding("X+V", CLIP_PASTE);
            addKeyBinding("X+X", CLIP_CUT);
            addKeyBinding("X+Z", UNDO);
            addKeyBinding("X+Y", REDO);
            addKeyBinding("X+S", SAVE);
            addKeyBinding("X+F", FIND);
        }
    }

    /**
     * Adds a key binding to this input handler. The key binding is a list of
     * white space separated key strokes of the form <i>[modifiers+]key</i>
     * where modifier is C for Control, A for Alt, or S for Shift, and key is
     * either a character (a-z) or a field name in the KeyEvent class prefixed
     * with VK_ (e.g., BACK_SPACE)
     * 
     * @param keyBinding
     *            The key binding
     * @param action
     *            The action
     */
    public void addKeyBinding(String keyBinding, ActionListener action)
    {
        Hashtable current = bindings;

        StringTokenizer st = new StringTokenizer(keyBinding);
        while (st.hasMoreTokens())
        {
            KeyStroke keyStroke = parseKeyStroke(st.nextToken());
            if (keyStroke == null)
                return;

            if (st.hasMoreTokens())
            {
                Object o = current.get(keyStroke);
                if (o instanceof Hashtable)
                    current = (Hashtable) o;
                else
                {
                    o = new Hashtable();
                    current.put(keyStroke, o);
                    current = (Hashtable) o;
                }
            }
            else
                current.put(keyStroke, action);
        }
    }

    /**
     * Removes a key binding from this input handler. This is not yet
     * implemented.
     * 
     * @param keyBinding
     *            The key binding
     */
    public void removeKeyBinding(String keyBinding)
    {
        throw new InternalError("Not yet implemented");
    }

    /**
     * Removes all key bindings from this input handler.
     */
    public void removeAllKeyBindings()
    {
        bindings.clear();
    }

    /**
     * Returns a copy of this input handler that shares the same key bindings.
     * Setting key bindings in the copy will also set them in the original.
     */
    public InputHandler copy()
    {
        return new DefaultInputHandler(this);
    }

    /**
     * Handle a key pressed event. This will look up the binding for the key
     * stroke and execute it.
     */
    public void keyPressed(KeyEvent evt)
    {
        int keyCode = evt.getKeyCode();
        int modifiers = evt.getModifiers();

        if (keyCode == KeyEvent.VK_CONTROL || keyCode == KeyEvent.VK_SHIFT || keyCode == KeyEvent.VK_ALT || keyCode == KeyEvent.VK_META)
            return;

        if ((modifiers & ~KeyEvent.SHIFT_MASK) != 0 || evt.isActionKey() || keyCode == KeyEvent.VK_BACK_SPACE || keyCode == KeyEvent.VK_DELETE || keyCode == KeyEvent.VK_ENTER || keyCode == KeyEvent.VK_TAB || keyCode == KeyEvent.VK_ESCAPE)
        {
            if (grabAction != null)
            {
                handleGrabAction(evt);
                return;
            }

            KeyStroke keyStroke = KeyStroke.getKeyStroke(keyCode, modifiers);
            Object o = currentBindings.get(keyStroke);
            if (o == null)
            {
                // Don't beep if the user presses some
                // key we don't know about unless a
                // prefix is active. Otherwise it will
                // beep when caps lock is pressed, etc.
                if (currentBindings != bindings)
                {
                    Toolkit.getDefaultToolkit().beep();
                    // F10 should be passed on, but C+e F10
                    // shouldn't
                    repeatCount = 0;
                    repeat = false;
                    evt.consume();
                }
                currentBindings = bindings;
                return;
            }
            else if (o instanceof ActionListener)
            {
                currentBindings = bindings;

                executeAction(((ActionListener) o), evt.getSource(), null);

                evt.consume();
                return;
            }
            else if (o instanceof Hashtable)
            {
                currentBindings = (Hashtable) o;
                evt.consume();
                return;
            }
        }
    }

    /**
     * Handle a key typed event. This inserts the key into the text area.
     */
    public void keyTyped(KeyEvent evt)
    {
        int modifiers = evt.getModifiers();
        char c = evt.getKeyChar();
        if (c != KeyEvent.CHAR_UNDEFINED && (modifiers & KeyEvent.META_MASK) == 0)
        {
            if (c >= 0x20 && c != 0x7f)
            {
                KeyStroke keyStroke = KeyStroke.getKeyStroke(Character.toUpperCase(c));
                Object o = currentBindings.get(keyStroke);

                if (o instanceof Hashtable)
                {
                    currentBindings = (Hashtable) o;
                    return;
                }
                else if (o instanceof ActionListener)
                {
                    currentBindings = bindings;
                    executeAction((ActionListener) o, evt.getSource(), String.valueOf(c));
                    return;
                }

                currentBindings = bindings;

                if (grabAction != null)
                {
                    handleGrabAction(evt);
                    return;
                }

                // 0-9 adds another 'digit' to the repeat number
                if (repeat && Character.isDigit(c))
                {
                    repeatCount *= 10;
                    repeatCount += (c - '0');
                    return;
                }

                executeAction(INSERT_CHAR, evt.getSource(), String.valueOf(evt.getKeyChar()));

                repeatCount = 0;
                repeat = false;
            }
        }
    }

    /**
     * Converts a string to a keystroke. The string should be of the form
     * <i>modifiers</i>+<i>shortcut</i> where <i>modifiers</i> is any
     * combination of A for Alt, C for Control, S for Shift or M for Meta, and
     * <i>shortcut</i> is either a single character, or a keycode name from the
     * <code>KeyEvent</code> class, without the <code>VK_</code> prefix.
     * 
     * @param keyStroke
     *            A string description of the key stroke
     */
    public static KeyStroke parseKeyStroke(String keyStroke)
    {
        if (keyStroke == null)
            return null;
        int modifiers = 0;
        int index = keyStroke.indexOf('+');
        if (index != -1)
        {
            for (int i = 0; i < index; i++)
            {
                switch (Character.toUpperCase(keyStroke.charAt(i)))
                {
                case 'A':
                    modifiers |= InputEvent.ALT_MASK;
                    break;
                case 'C':
                    modifiers |= InputEvent.CTRL_MASK;
                    break;
                case 'M':
                    modifiers |= InputEvent.META_MASK;
                    break;
                case 'S':
                    modifiers |= InputEvent.SHIFT_MASK;
                    break;
                case 'X':
                    /*
                     * MIRTH-1225 and MIRTH-2019: Sometimes CTRL might not
                     * be the same as the OS defined menu shortcut key mask.
                     * For example, this is COMMAND on OSX. This special
                     * case uses that menu shortcut key mask.
                     */
                    modifiers |= Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
                    break;
                }
            }
        }
        String key = keyStroke.substring(index + 1);
        if (key.length() == 1)
        {
            char ch = Character.toUpperCase(key.charAt(0));
            if (modifiers == 0)
                return KeyStroke.getKeyStroke(ch);
            else
                return KeyStroke.getKeyStroke(ch, modifiers);
        }
        else if (key.length() == 0)
        {
            System.err.println("Invalid key stroke: " + keyStroke);
            return null;
        }
        else
        {
            int ch;

            try
            {
                ch = KeyEvent.class.getField("VK_".concat(key)).getInt(null);
            }
            catch (Exception e)
            {
                System.err.println("Invalid key stroke: " + keyStroke);
                return null;
            }

            return KeyStroke.getKeyStroke(ch, modifiers);
        }
    }

    // private members
    private Hashtable bindings;

    private Hashtable currentBindings;

    private DefaultInputHandler(DefaultInputHandler copy)
    {
        bindings = currentBindings = copy.bindings;
    }
}
